/*
 *  Copyright 2007 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.logic;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.common.util.QueryInfo;
import com.blandware.atleap.model.core.BaseObject;
import com.blandware.atleap.model.core.Sequence;
import com.blandware.atleap.model.core.SequenceItem;
import com.blandware.atleap.model.core.Page;
import com.blandware.atleap.service.core.SequenceManager;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.webapp.struts.ContentTilesRequestProcessor;
import com.blandware.atleap.webapp.struts.HeritableComponentDefinition;
import com.blandware.atleap.webapp.util.core.ApplicationResources;
import com.blandware.atleap.webapp.util.core.CacheUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.taglib.TagUtils;
import org.apache.struts.tiles.ComponentDefinition;
import org.apache.struts.tiles.TilesUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * <p>Tag for iterating over items of some sequence.
 * </p>
 * <p>
 * Firstly, a set of sequences is calculated: those are all sequences that
 * belong to given definition and page with given URI. Then, among them,
 * sequences with given identifier are taken. Highest of these sequences is
 * iterated over.
 * </p>
 * <p><em>If URI is specified explicitly (through <b>uri</b> attribute), only
 * fields from page are considered: nothing is searched on layouts.</em></p>
 * <p>
 * For each sequence item object to which it points is being exported to
 * specified variable and body is rendered.
 * </p>
 * <p>Attributes:
 * <ul>
 * <li>
 * <b>identifier</b> - identifier of sequence; this is required
 * </li>
 * <li>
 * <b>definition</b> - name of tiles definition to which sequence may belong. If
 * not specified, definition of current page is taken
 * </li>
 * <li>
 * <b>uri</b> - URI of page to which sequence may belong. If not specified, URI
 * of current page is taken.
 * If this attribute is specified, fields are not searched on layouts.
 * </li>
 * <li>
 * <b>var</b> - name of variable that will accept object to which current item
 * points; this is required
 * </li>
 * <li>
 * <b>scope</b> - scope of variable that will accept object to which current
 * item points
 * </li>
 * <li>
 * <b>ignore</b> - If "true", then errors will be ignored (for instance, when
 * no definition is found at all, or no page with given URI exists). This can
 * be "true" or "false", default is "false".
 * </li>
 * <li>
 * <b>sequenceIdVar</b> - name of variable that will accept ID of sequence
 * </li>
 * <li>
 * <b>sequenceIdScope</b> - scope of variable that will accept ID of sequence
 * </li>
 * <li>
 * <b>sequenceItemIdVar</b> - name of variable to which ID of sequence item will
 * be exported on each iteration
 * </li>
 * <li>
 * <b>sequenceItemIdScope</b> - scope of variable to which ID of sequence item
 * will be exported on each iteration
 * </li>
 * </ul>
 * </p>
 * <p>
 * Here's an example:
 * <pre>
 * &lt;atleap:sequenceIterator identifier="sectionPages" var="sectionPage"&gt;
 *     &lt;div class="sectionElement"&gt;
 *         &lt;atleap:content identifier="title" uri="${sectionPage}"/&gt;
 *     &lt;/div&gt;
 * &lt;/atleap:sequenceIterator&gt;
 * </pre>
 * It will render a sequence of DIVs for each page which are in sequence named
 * "sectionPages" which belongs to current page or defined on its layout or
 * any of its ancestors.
 * </p>
 * <p><a href="SequenceIteratorTag.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.7 $ $Date: 2008/01/14 11:19:19 $
 * @jsp.tag name="sequenceIterator"
 * body-content="scriptless"
 */
public class SequenceIteratorTag extends SimpleTagSupport {

	protected transient final Log log = LogFactory.getLog(SequenceIteratorTag.class);
    protected ApplicationContext applicationCtx = null;

    /**
	 * Identifier of sequence
	 */
	protected String identifier = null;
    /**
     * Name of tiles definition to which sequence may belong. If not specified,
     * definition of current page is taken
     */
	protected String definition = null;
    /**
     * URI of page to which sequence may belong. If not specified, URI of current
     * page is taken
     */
	protected String uri = null;
    /**
	 * Name of variable to export object on each iteration
	 */
	protected String var;
	/**
	 * Scope to export variable to
	 */
	protected String scope;
    /**
     * Whether errors like absent definition/page will be considered errors or
     * not
     */
	protected Boolean ignore = Boolean.FALSE;
    /**
     * Name of variable to which ID of current sequence will be exported
     */
    protected String sequenceIdVar;
    /**
     * Scope of variable to which ID of current sequence will be exported
     */
    protected String sequenceIdScope;
    /**
     * Name of variable to which ID of current sequence item will be exported
     * on each iteration
     */
    protected String sequenceItemIdVar;
    /**
     * Scope of variable to which ID of current sequence item will be exported
     * on each iteration
     */
    protected String sequenceItemIdScope;

    protected Iterator sequenceIterator = null;

    /**
     * Returns identifier of sequence.
     *
	 * @return identifier
	 * @jsp.attribute required="true"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Identifier of sequence"
	 */
	public String getIdentifier() {
		return identifier;
	}

    /**
     * Sets identifier of sequence.
     *
     * @param identifier identifier to set
     */
	public void setIdentifier(String identifier) {
		this.identifier = identifier;
	}

	/**
     * Returns URI of page to which sequence belongs.
     *
     * @return URI of owner
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="URI of the page in database. By default it is uri of current processed page."
	 */
	public String getUri() {
		return uri;
	}

    /**
     * Sets URI of page to which sequence belongs.
     *
     * @param uri URI of owner to set
     */
	public void setUri(String uri) {
		this.uri = uri;
	}

	/**
     * Returns tiles definition name of layout to which sequence belogns.
     *
     * @return tiles definition name
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Definition name in database. By default it is definition of current processed page."
	 */
	public String getDefinition() {
		return definition;
	}

    /**
     * Sets tiles definition name of layout to which sequence belogns.
     *
     * @param definition tiles definition name to set
     */
	public void setDefinition(String definition) {
		this.definition = definition;
	}

    /**
     * Returns name of variable to export object on each iteration.
     *
     * @return name of variable to export object
     * @jsp.attribute required="true"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Name of variable to export object on each iteration"
     */
    public String getVar() {
        return var;
    }

    /**
     * Sets name of variable to export object on each iteration.
     *
     * @param var value to set
     */
    public void setVar(String var) {
        this.var = var;
    }

    /**
     * Returns scope of variable to export object on each iteration.
     *
     * @return scope of variable to export object
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Scope of variable to export object on each iteration"
     */
    public String getScope() {
        return scope;
    }

    /**
     * Sets scope of variable to export object on each iteration.
     *
     * @param scope value to set
     */
    public void setScope(String scope) {
        this.scope = scope;
    }

	/**
     * Returns whether to ignore errors.
     *
     * @return whether to ignore errors
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="If it is true errors like definition absence will be ignored"
	 */
	public Boolean getIgnore() {
		return ignore;
	}

    /**
     * Sets whether to ignore errors.
     *
     * @param ignore whether to ignore errors
     */
	public void setIgnore(Boolean ignore) {
		this.ignore = ignore;
	}

    /**
     * Returns name of variable to which ID of current sequence will
     * be exported. 
     *
     * @return name of variable for sequence ID
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Name of variable to which ID of current sequence will be exported"
     */
    public String getSequenceIdVar() {
        return sequenceIdVar;
    }

    /**
     * Sets name of variable to which ID of current sequence will be exported.
     *
     * @param sequenceIdVar variable name
     */
    public void setSequenceIdVar(String sequenceIdVar) {
        this.sequenceIdVar = sequenceIdVar;
    }

    /**
     * Returns scope of variable to which ID of current sequence will
     * be exported.
     *
     * @return scope of variable for sequence ID
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Scope of variable to which ID of current sequence will be exported"
     */
    public String getSequenceIdScope() {
        return sequenceIdScope;
    }

    /**
     * Sets scope of variable to which ID of current sequence will be exported.
     *
     * @param sequenceIdScope   scope
     */
    public void setSequenceIdScope(String sequenceIdScope) {
        this.sequenceIdScope = sequenceIdScope;
    }

    /**
     * Returns name of variable to which ID of current sequence item will
     * be exported on each iteration.
     *
     * @return name of variable for sequence item ID
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Name of variable to which ID of current sequence item will be exported on each iteration"
     */
    public String getSequenceItemIdVar() {
        return sequenceItemIdVar;
    }

    /**
     * Sets name of variable to which ID of current sequence item will
     * be exported on each iteration.
     *
     * @param sequenceItemIdVar variable name
     */
    public void setSequenceItemIdVar(String sequenceItemIdVar) {
        this.sequenceItemIdVar = sequenceItemIdVar;
    }

    /**
     * Returns scope of variable to which ID of current sequence item will
     * be exported on each iteration.
     *
     * @return scope of variable for sequence item ID
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Scope of variable to which ID of current sequence item will be exported on each iteration"
     */
    public String getSequenceItemIdScope() {
        return sequenceItemIdScope;
    }

    /**
     * Sets scope of variable to which ID of current sequence item will
     * be exported on each iteration.
     *
     * @param sequenceItemIdScope   scope
     */
    public void setSequenceItemIdScope(String sequenceItemIdScope) {
        this.sequenceItemIdScope = sequenceItemIdScope;
    }

    /**
	 * Constructs an iterator for indexed looping through the body once per
     * element.
	 *
	 * @throws javax.servlet.jsp.JspTagException
	 *          if a JSP exception has occurred
	 */
	public void doTag() throws JspTagException, IOException {

		PageContext pageContext = (PageContext) getJspContext();

		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		ApplicationResources applicationResources = ApplicationResources.getInstance(pageContext.getServletContext());
		ServletContext servletContext = pageContext.getServletContext();

		if ( ignore == null ) {
			ignore = Boolean.FALSE;
		}

		if ( applicationCtx == null ) {
			applicationCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(pageContext.getServletContext());
		}

		// get definition
		if ( definition != null ) {
			ComponentDefinition tmpDef = null;
			try {
				tmpDef = TilesUtil.getDefinition(definition, request, servletContext);
			} catch ( Exception ex ) {
				//do nothing
			}
			if ( tmpDef == null ) {
				if ( ignore.booleanValue() ) {
					return;
				}
				String errorMessage = "Specified definition '" + definition + "' has not been found";
                throw new JspTagException(errorMessage);
			}
		} else {
			definition = (String) pageContext.getAttribute(ContentTilesRequestProcessor.DEFINITION_NAME, PageContext.REQUEST_SCOPE);
		}
		if ( definition == null ) {
			if ( ignore.booleanValue() ) {
				return;
			}
			String errorMessage = "DEFINITION_NAME attribute has not been found in request. This can be used only on pages processed by action using tile definition.";
            throw new JspTagException(errorMessage);
		}

		//get uri
        boolean uriGiven = uri != null;
        if ( uri == null ) {
			uri = (String) pageContext.getAttribute(ContentTilesRequestProcessor.PROCESSED_URI, PageContext.REQUEST_SCOPE);
		}

		if ( uri == null ) {
			if ( ignore.booleanValue() ) {
				return;
			}
			String errorMessage = "PROCESSED_URI attribute has not been found in request. This can be used only on pages processed by action using tile definition.";
            throw new JspTagException(errorMessage);
		}

        // save definition and uri for later usage
		String savedDefinition = definition;
		String savedUri = uri;

		TagUtils tagUtils = TagUtils.getInstance();
        SequenceManager sequenceManager = (SequenceManager) applicationCtx.getBean(Constants.SEQUENCE_MANAGER_BEAN);
        try {
			if ( log.isDebugEnabled() ) {
				log.debug("Processing sequence; definition name: " + definition + ", uri: " + uri + ", identifier: " + identifier);
			}
			Long sequenceId = null;
            boolean sequenceNotFound = false;

            CacheUtil cacheUtil = CacheUtil.getInstance(request);
			CacheUtil.SequenceData sequenceData;

			//try to search in cache by uri
			sequenceData = cacheUtil.getPageSequenceFromCache(uri, identifier);

			if ( sequenceData != null ) {
				sequenceId = sequenceData.getId();
            }

			// not found in cache, search in persistence layer
			if ( sequenceData == null ) {
				// try to search content by uri
				if ( log.isDebugEnabled() ) {
					log.debug("Trying to search in persistent storage by uri: " + uri);
				}

				// search sequence by page uri
				boolean continueSearch = true;
				Sequence sequence = sequenceManager.findSequenceByPageUri(identifier, uri);
				if ( sequence != null ) {
                    // sequence is found, so stop searching
                    continueSearch = false;
				}

				if ( continueSearch && !uriGiven ) {
					// not found - search by layout definition
					String previousDefinition;
					do {
						previousDefinition = definition;
						definition = ((HeritableComponentDefinition) TilesUtil.getDefinition(definition, request, servletContext)).getExtends();
						if ( definition != null ) {
							if ( log.isDebugEnabled() ) {
								log.debug("Trying to search in persistent storage by definition: " + definition);
							}
							sequence = sequenceManager.findSequenceByLayoutDefinition(identifier, definition);
                            continueSearch = sequence == null;
						}
					} while ( definition != null && continueSearch );

					if ( sequence != null ) {
						sequenceId = sequence.getId();

                        // put in cache
                        sequenceData = new CacheUtil.SequenceData(sequenceId);
                        if ( definition == null ) {
                            cacheUtil.putLayoutSequenceInCache(sequenceData, previousDefinition, uri, identifier);
                        } else {
                            cacheUtil.putLayoutSequenceInCache(sequenceData, definition, uri, identifier);
                        }
					}
				} else if ( sequence != null ) {
                    // sequence is found on page
                    sequenceId = sequence.getId();

                    // put sequence in cache
                    sequenceData = new CacheUtil.SequenceData(sequenceId);
                    cacheUtil.putPageSequenceInCache(sequenceData, uri, identifier);
				}
                if (sequence == null) {
                    sequenceNotFound = true;
                }
            }

            if ( sequenceNotFound && uriGiven ) {
                // nothing was found, check maybe there's no page with given URI
                PageManager pageManager = (PageManager) applicationCtx.getBean(Constants.PAGE_MANAGER_BEAN);
                Page page = pageManager.findPageByUri(uri);
                if ( page == null ) {
                    if ( ignore.booleanValue() ) {
                        return;
                    }
                    String errorMessage = "Specified page uri '" + uri + "' has not been found";
                    throw new JspTagException(errorMessage);
                }
		    }

            if (!sequenceNotFound) {

                // saving sequence ID to context, if requested
                if (sequenceIdVar != null) {
                    int sequenceIdVarScope = PageContext.PAGE_SCOPE;
                    if ( sequenceIdScope != null ) {
                        sequenceIdVarScope = tagUtils.getScope(sequenceIdScope);
                    }
                    pageContext.setAttribute(sequenceIdVar, sequenceId, sequenceIdVarScope);
                }

                QueryInfo queryInfo = new QueryInfo();
                Map queryParameters = new HashMap();
                queryParameters.put("sequenceId", sequenceId);
                queryParameters.put("useCacheRegion", Boolean.TRUE);
                queryInfo.setQueryParameters(queryParameters);
                PartialCollection sequenceItems = sequenceManager.listSequenceItems(queryInfo);

                if (sequenceItems != null) {
                    sequenceIterator = sequenceItems.iterator();
                }

                if ( sequenceIterator != null
                        && sequenceIterator.hasNext() ) {
                    int varScope = PageContext.PAGE_SCOPE;
                    if ( scope != null ) {
                        varScope = tagUtils.getScope(scope);
                    }
                    while ( sequenceIterator.hasNext() ) {
                        SequenceItem sequenceItem = (SequenceItem) sequenceIterator.next();

                        // saving sequence item ID to context, if requested
                        if (sequenceItemIdVar != null) {
                            int sequenceItemIdVarScope = PageContext.PAGE_SCOPE;
                            if ( sequenceItemIdScope != null ) {
                                sequenceItemIdVarScope = tagUtils.getScope(sequenceItemIdScope);
                            }
                            pageContext.setAttribute(sequenceItemIdVar, sequenceItem.getId(), sequenceItemIdVarScope);
                        }

                        BaseObject object;
                        if (sequenceItem.getPage() != null) {
                            object = sequenceItem.getPage();
                        } else {
                            object = sequenceItem.getContentResource();
                        }
                        pageContext.setAttribute(var, object, varScope);
                        if ( log.isDebugEnabled() ) {
                            log.debug("Putting as attribute with key=" + var + " and value=" + sequenceItem);
                        }
                        JspFragment body = getJspBody();
                        if ( body != null ) {
                            body.invoke(null);
                        }
                    }
                }
            } else {
                if (!ignore.booleanValue()) {
                    String errorMessage = applicationResources.getMessage(request, "core.commons.sequenceIteratorTag.sequenceNotFound", new Object[]{identifier, savedUri, savedDefinition});
                    tagUtils.write(pageContext, errorMessage);
                }
            }

        } catch ( Exception ex ) {
            throw new JspTagException(ex);
		}
    }
}
